/**
 * 
 */
package br.unifor.mia.cd.middleware.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import arcademis.MarshalException;
import arcademis.Stream;
import br.unifor.mia.cd.middleware.arcademis.marshalable.DefaultMarshaller;
import br.unifor.mia.cd.middleware.arcademis.marshalable.Marshalable;
import br.unifor.mia.cd.middleware.arcademis.marshalable.Marshaller;
import br.unifor.mia.cd.middleware.arcademis.marshalable.MarshallerDecorator;

/**
 * @author arthurlandim
 *
 */
public class MarshalableUtil {
	
	public static List<Field> sortListField(Object obj) {
		Field[] fieldArray = obj.getClass().getDeclaredFields();
		List<Field> fieldList = new ArrayList<Field>();
		for (Field field : fieldArray) {
			fieldList.add(field);
		}
		Collections.sort(fieldList, new FieldComparator());
		return fieldList;
	}
	
	/**
	 * Pegar o valor de um atributo de um campo.
	 * Uso o BeanUtils. Serve apenas para encapsular as exceptions que podem ser lançadas
	 * @param bean
	 * @param fieldName
	 * @return
	 */
	public static Object getProperty(Object bean, String fieldName) {
		Object result = null;
		try {
			String fieldMethodGet = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
			Method method = bean.getClass().getDeclaredMethod(fieldMethodGet, new Class[0]);
			result = method.invoke(bean, new Object[0]);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}
	
	/**
	 * Pegar o valor de um atributo de um campo.
	 * Uso o BeanUtils. Serve apenas para encapsular as exceptions que podem ser lançadas
	 * @param bean
	 * @param fieldName
	 * @return
	 */
	public static Object setProperty(Object bean, String fieldName, Object value) {
		Object result = null;
		try {
			Field field = bean.getClass().getDeclaredField(fieldName);
			String fieldMethodGet = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
			Method method = bean.getClass().getDeclaredMethod(fieldMethodGet, new Class[]{field.getType()});
			result = method.invoke(bean, new Object[]{value});
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}
	
	/**
	 * Retorna o serializador a ser utilizado
	 * @param annotationClazz
	 * @return serializador
	 */
	public static Marshaller getMarshaller(Marshalable annotationClazz) {
		return overrideMarshaller(annotationClazz, new DefaultMarshaller());
	}
	
	/**
	 * Método para criar objetos decoradores de serialização
	 * @param annotationClazz
	 * @param marshaller
	 * @return serializador aninhado com decoradores
	 */
	private static Marshaller overrideMarshaller(Marshalable annotationClazz, Marshaller marshaller) {
		Marshaller result = marshaller;
		Constructor<?> constructor = null;
		if (annotationClazz.decorator() != null) {
			Class<? extends MarshallerDecorator> decoratorClazz = annotationClazz.decorator();
			try {
				constructor = decoratorClazz.getConstructor(new Class[]{Marshaller.class});
				result = (Marshaller) constructor.newInstance(new Object[]{result});
			} catch (Exception e) {
				System.out.println("Decorator não implementa construtor recebendo Marshaller");
			}
		} else if (annotationClazz.decorators() != null) {
			Class<? extends MarshallerDecorator>[] decoratorsClazz = annotationClazz.decorators();
			for (Class<? extends MarshallerDecorator> decoratorClazz : decoratorsClazz) {
				try {
					constructor = decoratorClazz.getConstructor(new Class[]{Marshaller.class});
					result = (Marshaller) constructor.newInstance(new Object[]{result});
				} catch (Exception e) {
					System.out.println("Decorator não implementa construtor recebendo Marshaller");
				}
			}
		}
		return null;
	}

	/**
	 * Método para ser utilizado para serializar um objeto com a anotação <br>
	 * {@link Marshalable}.
	 * @param stream
	 * @param bean
	 * @param marshaller
	 * @throws MarshalException
	 */
	public static void marshalingFields(Stream stream, Object bean,
			Marshaller marshaller) throws MarshalException {
		marshalingFields(stream, bean, marshaller, Boolean.TRUE);
	}
	
	/**
	 * Método para ser utilizado para deserializar um objeto com a anotação <br>
	 * {@link Marshalable}.
	 * @param stream
	 * @param bean
	 * @param marshaller
	 * @throws MarshalException
	 */
	public static void unmarshalingFields(Stream stream, Object bean,
			Marshaller marshaller) throws MarshalException {
		marshalingFields(stream, bean, marshaller, Boolean.FALSE);
	}
	
	/**
	 * Método para ser utilizado para serializar e deserializar um objeto com a anotação <br>
	 * {@link Marshalable}.
	 * @param stream Stream utilizada
	 * @param bean Objeto a ser de/serializado
	 * @param marshaller Marshaller da classe
	 * @param marshalling Se true, então é serialização, senão, é deserialização
	 * @throws MarshalException
	 */
	private static void marshalingFields(Stream stream, Object bean,
			Marshaller marshaller, boolean marshalling) throws MarshalException {
		//Pegando lista de atributos ordenada pelo nome
		List<Field> fieldList = MarshalableUtil.sortListField(bean);
		
		Marshaller fieldMarshaller = null;
		for (Field field : fieldList) {
			fieldMarshaller = marshaller;
			
			/*
			 * Verificando se os atributos da classe possuem atributos
			 * com a anotação setando outros decoradores.
			 */
			if (field.isAnnotationPresent(Marshalable.class)) {
				Marshalable annotationMarshalable = field.getAnnotation(Marshalable.class);
				//Aninhar os decoradores
				fieldMarshaller = overrideMarshaller(annotationMarshalable, marshaller);
			}

			//Verificar se está serializando ou não
			if (marshalling) {
				//Caso esteja, então buscar o valor codificado pelos decoradores e 
				//setar na stream
				Object fieldMarshallValue = fieldMarshaller.getFieldMarshalValue(field.getName(), bean);
				callStreamSpecificWriteMethod(stream, fieldMarshallValue, field.getType());
			} else {
				//Caso estaja deserializando, então pegar o valor da stream e
				//utilizar os decoradores para decodificar
				Object fieldValueFromStream = callStreamSpecificReadMethod(stream, field.getType());
				Object fieldValue = fieldMarshaller.getFieldUnMarshalValue(field.getName(), bean, fieldValueFromStream);
				setProperty(bean, field.getName(), fieldValue);
			}

		}			
	}
	
	/**
	 * Workaround para resolver problema de reflexão em polimorfismo.
	 * @param stream
	 * @param param
	 * @param paramClass
	 * @throws MarshalException
	 */
	private static void callStreamSpecificWriteMethod(Stream stream, Object param, Class<?> paramClass) throws MarshalException {
		//Sei que é estranho, mas isso existe por um motivo...
		if (paramClass.isAssignableFrom(String.class)) {
			stream.write((String) param);
		} else if (paramClass.isAssignableFrom(Integer.class)) {
			stream.write(((Integer) param).intValue());
		} else if (paramClass.isAssignableFrom(Boolean.class)) {
			stream.write(((Boolean) param).booleanValue());
		} else if (paramClass.isAssignableFrom(Byte.class)) {
			stream.write(((Byte) param).byteValue());
		} else if (paramClass.isAssignableFrom(Character.class)) {
			stream.write(((Character) param).charValue());
		} else if (paramClass.isAssignableFrom(Long.class)) {
			stream.write(((Long) param).longValue());
		} else if (paramClass.isAssignableFrom(Short.class)) {
			stream.write(((Short) param).shortValue());
		} else if (paramClass.isAssignableFrom(Exception.class)) {
			stream.write((Exception) param);
		} else {
			//TODO: Descomentar quando o método da stream com parametro Object estiver pronto
//			stream.write(param);
		}
	}
		
	/**
	 * Workaround para resolver problema de reflexão em polimorfismo.
	 * @param stream
	 * @param param
	 * @param paramClass
	 * @throws MarshalException
	 */
	private static Object callStreamSpecificReadMethod(Stream stream, Class<?> paramClass) throws MarshalException {
		//Sei que é estranho, mas isso existe por um motivo...
		Object result = null;
		if (paramClass.isAssignableFrom(String.class)) {
			result = (String) stream.readObject();
		} else if (paramClass.isAssignableFrom(Integer.class)) {
			result = new Integer(stream.readInt());
		} else if (paramClass.isAssignableFrom(Boolean.class)) {
			result = new Boolean(stream.readBoolean());
		} else if (paramClass.isAssignableFrom(Byte.class)) {
			result = new Byte(stream.readByte());
		} else if (paramClass.isAssignableFrom(Character.class)) {
			result = new Character(stream.readChar());
		} else if (paramClass.isAssignableFrom(Long.class)) {
			result = new Long(stream.readLong());
		} else if (paramClass.isAssignableFrom(Short.class)) {
			result = new Short(stream.readShort());
		} else if (paramClass.isAssignableFrom(Exception.class)) {
			result = (Exception) stream.readObject();
		} else {
			result = stream.readObject();
		}
		return result;
	}
}
